Skip to content

Add asset images, AsyncImage, and content modes - #53

Merged
colemancda merged 9 commits into
masterfrom
feature/images
Jul 24, 2026
Merged

Add asset images, AsyncImage, and content modes#53
colemancda merged 9 commits into
masterfrom
feature/images

Conversation

@colemancda

Copy link
Copy Markdown
Member

Image previously resolved only a curated ~25-symbol Material map; anything else rendered as a [name] placeholder, and there was no way to show a remote image. This closes that gap.

What's new

  • Asset imagesImage("name") resolves against the app's drawable resources (the Android analog of an asset-catalog lookup), falling back to the existing placeholder when there's no match.
  • Image.resizable() plus .scaledToFit() / .scaledToFill() / .aspectRatio(contentMode:). resizable() is a method on Image itself, as in SwiftUI; without it an image keeps its natural size.
  • AsyncImage(url:) — fetch, decode, and swap in, entirely Compose-side: a spinner while loading, a labeled placeholder on failure. Loading never touches the bridge, and the effect is keyed by URL so a new URL reloads.

Both loaders sit behind an expect/actual seam (rememberAssetPainter, loadRemoteImage), mirroring how VideoPlayer is structured, so the common interpreter stays free of platform APIs.

A bug the device caught

My first loader used URL.openStream(), and the real remote image failed while the deliberately-invalid one "passed". openStream() sends the default Java/<version> user agent, which a number of CDNs reject outright — I confirmed Wikimedia answers 403 to it. It also doesn't follow cross-protocol redirects or treat a non-200 as failure, so an error page would have been decoded as garbage. Replaced with an explicit HttpURLConnection that sets a real User-Agent and Accept, follows up to 5 redirects manually, and fails on any non-200.

Verification

  • swift test — 3 new tests (symbol vs. named asset; resizable + each content mode; AsyncImage URL incl. the nil case), 82 total passing
  • Emulator (new "Images" catalog screen), all screenshot-checked:
    • the bundled drawable renders at natural size
    • fit vs. fill are visibly different — fit letterboxes the square source in a 160×90 frame, fill crops it
    • AsyncImage loads a real remote photo, and "Load the other image" swaps in a different one, confirming the URL re-key
    • the invalid URL settles on [failed: …] rather than spinning forever

Scope

AsyncImage's content/placeholder closure overloads aren't included (this is the plain AsyncImage(url:) form), and there's no memory/disk cache — each composition of a new URL fetches. Named assets don't resolve on the desktop target, which has no resource bundle.

@colemancda
colemancda merged commit 9f5d3a4 into master Jul 24, 2026
6 checks passed
@colemancda
colemancda deleted the feature/images branch July 24, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant